package com.github.llyb120.mugen;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import lombok.NonNull;
import lombok.Synchronized;
import lombok.experimental.NonFinal;
import net.sf.cglib.proxy.*;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;


public class PluginRunner {


    private volatile Map<String, ExecutableMethod> executableMap = new ConcurrentHashMap<>();
    private volatile Map<String, List<AnalyzedMethod>> analyzedMap = new ConcurrentHashMap<>();
    private volatile Map<String, Object> pluginMap = new ConcurrentHashMap<>();

    private PluginLoader pluginLoader;
    private PluginOption pluginOption;
    private ThreadLocal<Chain> localContext = new ThreadLocal() {
    };


//    String pluginDir;// = new File(("D:\\work\\cal-engine\\target\\classes\\com\\github\\llyb120\\workflow\\plugin")).getAbsolutePath();


    private PluginRunner() {
    }

    private PluginRunner(@NonNull PluginOption pluginOption) {
        this.pluginOption = pluginOption;
        //默认
        pluginOption.getLoaders().add(new ClassFileLoader());
        pluginOption.getInjects().put(toLowerName(PluginRunner.class.getSimpleName()), this);

        refresh();
    }

    /**
     * 重新加载模型
     */
    @Synchronized
    public void refresh() {
        if (pluginOption.getPluginDir() == null) {
            throw new RuntimeException();
        }
        destroyPlugins();
        loadPlugins();
    }


    private void injectBeans(Object instance) throws IllegalAccessException {
        //参数注入
        for (Field field : instance.getClass().getSuperclass().getDeclaredFields()) {
            Resource resource = field.getAnnotation(Resource.class);
            if (resource == null) {
                continue;
            }
            String name = resource.name();
            if (name.isEmpty()) {
                name = toLowerName(field.getType().getSimpleName());
            }
            Object bean = pluginOption.getInjects().get(name);
            if (bean != null) {
                field.setAccessible(true);
                field.set(instance, bean);
            }
        }
    }


    private String toLowerName(@NonNull String str) {
        if (str.isEmpty()) {
            return str;
        }
        return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
    }


    /**
     * 初始化插件
     */
    private void initlizePlugin() {
        List<AnalyzedMethod> list = analyzedMap.get("_OnLoad");
        if (list == null) {
            return;
        }
        for (AnalyzedMethod analyzedMethod : list) {
            analyzedMethod.invoke();
        }
    }

    /**
     * 摧毁插件
     */
    private void destroyPlugins() {
        List<AnalyzedMethod> list = analyzedMap.get("_OnDestroy");
        if (list == null) {
            return;
        }
        for (AnalyzedMethod analyzedMethod : list) {
            analyzedMethod.invoke();
        }
    }

    /**
     * 加载插件
     */
    private void loadPlugins() {
        //scan
        Map<String, ExecutableMethod> exportMethodMap = new ConcurrentHashMap<>();
        Map<String, List<AnalyzedMethod>> analyzedMap = new ConcurrentHashMap<>();
        Map<String, Object> pluginMap = new ConcurrentHashMap<>();

        Map<String, ByteClass> byteClassMap = new ConcurrentHashMap<>();
        pluginLoader = new PluginLoader(pluginOption, byteClassMap);
        Map<String, List<File>> files = new HashMap<>();
        FileUtil.walkFiles(pluginOption.getPluginDir().toPath(), 10, new SimpleFileVisitor<Path>() {
            @Overrule
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                File fi = file.toFile();
                String ext = FileUtil.extName(fi);
                List<File> list = files.computeIfAbsent(ext, k -> new ArrayList<>());
                list.add(fi);
//                scan: {
//                    if(fi.getName().endsWith(".class")){
//                        ByteClass classfile = analyzeByteClassFile(fi);
//                        if (classfile != null) {
//                            byteClassMap.put(classfile.getClassName(), classfile);
//                        }
//                    }
//                }
                return super.visitFile(file, attrs);
            }
        });
        //按类型读取文件
        for (Map.Entry<String, List<File>> entry : files.entrySet()) {
            for (PluginFileLoader loader : pluginOption.getLoaders()) {
                if (loader.match(entry.getKey())) {
                    byteClassMap.putAll(loader.analyze(pluginOption, entry.getValue()));
                }
            }
        }
        for (ByteClass value : byteClassMap.values()) {
            try {
                Class<?> clz = pluginLoader.loadClass(value.getClassName());
                Plugin plugin = clz.getAnnotation(Plugin.class);
                if (plugin != null) {
                    //初始化
                    //proxy-start
                    Object pluginIns = createProxyInstance(clz);
                    String pluginName = plugin.value();
                    if (StrUtil.isBlank(pluginName)) {
                        pluginName = toLowerName(clz.getSimpleName());
                    }
                    pluginMap.put(pluginName, pluginIns);
                    //proxy-end
//                    Object pluginIns = clz.newInstance();
                    injectBeans(pluginIns);
                    for (Method method : clz.getDeclaredMethods()) {
                        //event
                        Class[] anclzs = {OnLoad.class, OnDestroy.class, Overrule.class};
                        for (Class anclz : anclzs) {
                            Annotation anItem = method.getAnnotation(anclz);
                            if (anItem != null) {
                                method.setAccessible(true);
                                AnalyzedMethod analyzedMethod = AnalyzedMethod.builder()
                                        .method(method)
                                        .clz(anclz)
                                        .source(anItem)
                                        .instance(pluginIns)
                                        .build();
                                List<AnalyzedMethod> list = analyzedMap.computeIfAbsent("_" + anclz.getSimpleName(), k -> new CopyOnWriteArrayList<>());
                                list.add(analyzedMethod);
                            }
                        }
                        //exec
                        Executable executable = method.getAnnotation(Executable.class);
                        if (executable != null) {
                            if (exportMethodMap.containsKey(executable.value())) {
                                throw new ExecutableMethodDupException();
                            }
                            ExecutableMethod executableMethod = ExecutableMethod.builder()
                                    .method(method)
                                    .executable(executable)
                                    .instance(pluginIns)
                                    .build();
                            exportMethodMap.put(executable.value(), executableMethod);
                        }
                    }
                }
            } catch (ClassNotFoundException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        this.executableMap = exportMethodMap;
        this.analyzedMap = analyzedMap;
        this.pluginMap = pluginMap;


        initlizePlugin();
    }


    private Object createProxyInstance(Class clz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(pluginLoader);
        enhancer.setSuperclass(clz);
        enhancer.setCallback(new MethodInterceptor() {
            @Overrule
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                Executable executable = method.getAnnotation(Executable.class);
                if (executable == null) {
                    return methodProxy.invokeSuper(o, objects);
                }
                Chain chain = null;
                try {
                    chain = localContext.get();
                    chain.pushChain(executable.value(), objects);
                    //原始参数后面都要用
                    List<String> parameterNames = Arrays.stream(method.getParameters()).map(e -> e.getName()).collect(Collectors.toList());
                    //参数有可能被修改，所以这里使用map里的新参数
                    Chain.ChainInvokable exp = (Object... args) -> {
                        //fix参数
                        for (int i = 0; i < args.length; i++) {
                            objects[i] = args[i];
                        }
                        return methodProxy.invokeSuper(o, objects);
                    };
                    chain.setChainInvokable(exp);
                    chain = doOverrule(chain, exp, parameterNames, objects);
                    return chain.invoke();
                } finally {
                    chain.popChain();
                }
                //do hook
                //argument map
//                HashMap<String,Object> map = new HashMap<>();
//                int i = 0;
//                //原始参数后面都要用
//                for (Parameter parameter : method.getParameters()) {
//                    map.put(parameter.getName(), objects[i++]);
//                }
//                String pre = ctx.getPreStr();
//                map.setModified(false);
//                doHook(pre, ctx);
//                if(map.getModified()){
//                    //如果参数被修改
//                    objects = convertParams(method, map);
//                }
                //如果被重写
//                Object ret = methodProxy.invokeSuper(o, objects);
//                String postStr = pre.replace("!", "");
//                doHook(postStr, ctx);
//                ctx.getNameStack().removeLast();
//                return null;
            }
        });
        return enhancer.create();
    }


    private Chain doOverrule(Chain chain, Chain.ChainInvokable sourceMethod, List<String> parameterNames, Object[] sourceArgs) {
        List<AnalyzedMethod> list = analyzedMap.get("_Overrule");
        if (list == null) {
            return chain;
        }
        AtomicReference<Chain> ret = new AtomicReference<>(chain);
        list = list.parallelStream()
                .filter(e -> chain.checkThrough((Overrule) e.getSource()))
                .sorted(new Comparator<AnalyzedMethod>() {
                    @Override
                    public int compare(AnalyzedMethod o1, AnalyzedMethod o2) {
                        return Integer.compare(((Overrule) o2.getSource()).order(), ((Overrule) o1.getSource()).order());
                    }
                })
                .collect(Collectors.toList());
        List<Chain> chains = new ArrayList<>();
        for (int i = 0; i < list.size(); i++) {
            Chain newChain = new Chain();
            newChain.setStack(chain.getStack());
            Chain _chain;
            if(i == 0){
                _chain = chain;
            } else {
                _chain = chains.get(i - 1);
            }
            AnalyzedMethod e = list.get(i);
            newChain.setChainInvokable((Object... _args) -> {
                //根据提供的新参数修正旧参数
                for (int i1 = 0; i1 < _args.length; i1++) {
                    sourceArgs[i1] = _args[i1];
                }
                Object[] args = new Object[e.getMethod().getParameterCount()];
                int j = 0;
                for (Parameter parameter : e.getMethod().getParameters()) {
                    if(parameter.getType() == Chain.class){
                        args[j] = _chain;
                    } else {
                        //如果存在新参数
                        int pos = parameterNames.indexOf(parameter.getName());
                        if(pos == -1){
                            args[j] = null;
                        } else {
                            args[j] = sourceArgs[pos];
                        }
                    }
                    j++;
                }
                return e.invoke(args);
            });
            chains.add(newChain);
        }
        return chains.isEmpty() ? chain : chains.get(chains.size() - 1);
    }


//        return execute(methodName, (Object) null);
//    }
//    @Deprecated
//    public <T> T execute(@NonNull String methodName, Map<String, Object> params) {
//        if (params == null) {
//            params = new HashMap<>();
//        }
//        ExecutableMethod executableMethod = executableMap.get(methodName);
//        if (executableMethod == null) {
//            throw new CannotFindHookMethodException();
//        }
//        Chain ctx = localContext.get();
//        try {
//            if (ctx == null) {
//                //没有上下文环境的时候才启用新的环境
//                localContext.set(new Chain());
//            }
//            return (T) executableMethod.invoke(params);
//        } finally {
//            if (ctx == null) {
//                localContext.set(null);
//            }
//        }
//    }

    public <T> T execute(@NonNull String methodName, Object... args) {
        ExecutableMethod executableMethod = executableMap.get(methodName);
        if (executableMethod == null) {
            throw new CannotFindHookMethodException();
        }
        Chain ctx = localContext.get();
        try {
            if (ctx == null) {
                //没有上下文环境的时候才启用新的环境
                localContext.set(new Chain());
            }
            return (T) executableMethod.invoke(args);
        } finally {
            if (ctx == null) {
                localContext.set(null);
            }
        }
    }


    /**
     * builder
     **/
    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {

        private PluginOption pluginOption = new PluginOption();

        Builder() {
        }

        public Builder pluginDir(String path) {
            pluginOption.setPluginDir(new File(path));
            return this;
        }

        public Builder basePackage(String basePackage) {
            pluginOption.setBasePackage(basePackage);
            return this;
        }

        public Builder inject(Class clz, Object object) {
            pluginOption.getInjects().put(toLowerName(clz.getSimpleName()), object);
            return this;
        }

        public Builder inject(String beanName, Object object) {
            pluginOption.getInjects().put(beanName, object);
            return this;
        }

        private String toLowerName(@NonNull String str) {
            if (str.isEmpty()) {
                return str;
            }
            return str.substring(0, 1).toLowerCase() + str.substring(1, str.length());
        }

        public Builder loader(PluginFileLoader loader) {
            pluginOption.getLoaders().add(loader);
            return this;
        }

        public PluginRunner build() {
            return new PluginRunner(pluginOption);
        }

    }

//    public void publish(String flowId, String uid) {
//        Workflow flow = findWorkflow(flowId);
//        FlowData data = FlowData
//                .builder()
//                .build();
//        //dealer
//        FlowDealer dealer = FlowDealer.builder()
//                .publishUid(uid)
//                .build();
//        data.setFlowDealer(dealer);
//        //startnode
//        Startnode startnode = findStartNode(flow);
//        data.getCurrentNodes().add(startnode.getId());
//        //plugin:pre-publish
//
//        //plugin:post-publish
//        save(data);
//    }
//
//    public void submit(String uid, String flowId, Map<String, Object> data) {
//        try{
//            lock(flowId);
//            FlowLog log = FlowLog.builder()
//                    .created(new Date())
//                    .uid(uid)
//                    .data(new HashMap<>(data))
//                    .build();
//            FlowData flowdata = findFlowData(flowId);
//            flowdata.getLogs().add(log);
//            //寻找下一个节点
//            //plugin:pre-submit
////            execute("submit");
//            //plugin:post-submit
//            save(flowdata);
//        } finally {
//            unlock(flowId);
//        }
//    }
//
//
//    public void next(){
//
//    }
//
//
//    public void save(FlowData data){
//        storage.save(data);
//    }


    /**
     * 查找一个任务模型
     *
     * @param id
     * @return
     */
//    public Workflow findWorkflow(String id) {
//        Workflow flow = gWorkflowMap.get(id);
//        if (flow == null) {
//            throw new CanNotFoundWorkflowException();
//        }
//        return flow;
//    }
//
//    public FlowData findFlowData(String id){
//        return storage.findFlowData(id);
//    }
//
//    public Startnode findStartNode(Workflow workflow){
//        return (Startnode) workflow.getNode()
//                .stream()
//                .filter(e -> e instanceof Startnode)
//                .findFirst()
//                .orElseThrow(() -> new CanNotFoundWorkflowException());
//    }
//
//
//    public ReentrantLock lock(String id){
//        ReentrantLock lock = gWorkflowLockMap.computeIfAbsent(id, k -> new ReentrantLock());
//        lock.lock();
//        return lock;
//    }
//
//    public ReentrantLock unlock(String id){
//        ReentrantLock lock = gWorkflowLockMap.computeIfAbsent(id, k -> new ReentrantLock());
//        if(lock.isLocked()){
//            lock.unlock();
//        }
//        return lock;
//    }
//    private ReentrantLock gLock = new ReentrantLock();
//    private volatile Map<String, Workflow> gWorkflowMap = new ConcurrentHashMap<>();
//    private volatile Map<String, ReentrantLock> gWorkflowLockMap = new ConcurrentHashMap<>();


}
